Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Components: Wire up reduxified country states #8414

Merged
merged 6 commits into from
Oct 6, 2016

Conversation

obenland
Copy link
Member

@obenland obenland commented Oct 3, 2016

Moves away from lib/states-list to using countryStates built on Redux.

To test:

  1. Go to My Sites.
  2. Under Domains, click Add.
  3. Hit Select for any domain to enter the checkout process.
  4. Skip adding Email support.
  5. In the Domain Contact Information form, select United States in the country dropdown. The State input field should have been replaced with a dropdown of states.

See #8202.
Fixes #7922.

@matticbot
Copy link
Contributor

@obenland
Copy link
Member Author

obenland commented Oct 3, 2016

My proposed changes don't work yet unfortunately. @gwwar can you point me to what I'm doing wrong?

Warning: There is an internal error in the React performance measurement code. Did not expect componentDidUpdate timer to start while render timer is still in progress for another instance.

Uncaught TypeError: this.translate is not a function in state-select.jsx:129

I wrapped StateSelect in a localize() call to get access to translate() but that doesn't seem to work.

It also seems that the fallback for when there are no states requires the query component to be called before checking for the existence of states, so I can't really use it "inline"?

@gwwar
Copy link
Contributor

gwwar commented Oct 3, 2016

I wrapped StateSelect in a localize() call to get access to translate() but that doesn't seem to work.

Almost! If we use the higher-order component localize to wrap StateSelect, translate is available as a component property.

So instead of

{ this.translate( 'foo' ); }

Usage becomes:

const { translate } = this.props;
//...
{ translate( 'foo' ) }

https://github.com/Automattic/i18n-calypso#localize

mixins: [ observe( 'statesList' ) ],

recordStateSelectClick: function() {
recordStateSelectClick() {
if ( this.props.eventFormName ) {
analytics.ga.recordEvent( 'Upgrades', `Clicked ${ this.props.eventFormName } State Select` );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is a connected component, let's use the recordGoogleEvent redux action . This makes it a lot easier to write any component tests.

So:

analytics.ga.recordEvent( 'Upgrades', `Clicked ${ this.props.eventFormName } State Select` );

turns into

const { recordGoogleEvent } = this.props;
recordGoogleEvent( 'Upgrades', `Clicked ${ this.props.eventFormName } State Select` );

}

query() {
return this.props.countryCode && <QueryCountryStates countryCode={ this.props.countryCode } />;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Usage seems a bit awkward. Maybe let's update QueryCountryStates to do a no-op if countryCode is falsey?

const classes = classNames( this.props.additionalClasses, 'state' ),
statesList = this.props.statesList.getByCountry( this.props.countryCode );
let options = [];
this.query();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's have the QueryComponent inline instead.

* Uses Redux based Google Analytics.
* Moves `QueryCountryStates` inline.
* Removes `lib/states-list`.
@obenland
Copy link
Member Author

obenland commented Oct 4, 2016

I think this one is ready for final review @gwwar @aduth. I've not been able to test it in the domain management's contact form since I don't have a site with a wp.com-based domain. Any tipps on how I can test that?

@obenland obenland added [Status] Needs Review The PR is ready for review. This also triggers e2e canary tests and wp-desktop tests automatically. and removed [Status] In Progress labels Oct 4, 2016
import React, { Component, PropTypes } from 'react';
import classNames from 'classnames';
import { connect } from 'react-redux';
import isEmpty from 'lodash/isEmpty';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As of #6539, we encourage importing from the root Lodash module directly:

import { isEmpty } from 'lodash';

analytics.ga.recordEvent( 'Upgrades', `Clicked ${ this.props.eventFormName } State Select` );
recordStateSelectClick( eventFormName ) {
if ( eventFormName ) {
recordGoogleEvent( 'Upgrades', `Clicked ${ eventFormName } State Select` );
Copy link
Contributor

@aduth aduth Oct 5, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're not dispatching an action here, only calling an action creator. The action creator will return a thunk, but you need to use react-redux's connect mapDispatchToProps to pass the action creator as a wrapped dispatch function prop into the component.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh sorry, forgot to add example connect code for that.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No reason to apologize, I should know better. I actually passed it to connect() at one point but for some reason decided to remove it again

value={ this.props.value }
disabled={ this.props.disabled }
onChange={ this.props.onChange }
onClick={ this.recordStateSelectClick.bind( this, this.props.eventFormName ) }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We've enabled the react/jsx-no-bind ESLint rule which this violates because bind will always create a new function, thereby preventing any possible benefits of pure render of a child component. Instead, you should bind components in the constructor:

constructor() {
    super( ...arguments );

    this.recordStateSelectClick = this.recordStateSelectClick.bind( this );
}

recordStateSelectClick() {
    const { eventForm } = this.props;
    // ...
}

... or stage 1 class instance fields:

recordStateSelectClick = () => {
    const { eventForm } = this.props;
    // ...
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My implementation currently looks like this:

recordStateSelectClick() {
    const { eventFormName, recordGoogleEvent } = this.props;

    if ( eventFormName ) {
        recordGoogleEvent( 'Upgrades', `Clicked ${ eventFormName } State Select` );
    }
}

This however gives me a no-shadow es-lint warning since I'm already importing recordGoogleEvent in the upper scope. Is there a better way to mitigate that than using this.props.recordGoogleEvent()?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm generally lazy and do this.props.recordGoogleEvent() but there's nothing stopping us from making another variable.

const { eventFromName, recordGoogleEvent: recordEvent } = this.props;
    if ( eventFormName ) {
        recordEvent( 'Upgrades', `Clicked ${ eventFormName } State Select` );
    }

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ha! I tried const { eventFromName, recordGoogleEvent as recordEvent } = this.props;. Learned something yet again :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea it can be confusing since the import syntax is similar but not quite the same (i.e. import { recordGoogleEvent as recordEvent } from ''; would be correct).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's what I thought of :)

{ isEmpty( this.props.countryStates )
? <Input ref="input" { ...this.props } />
: <div className={ classes }>
<FormLabel htmlFor={ this.props.name }>{ this.props.label }</FormLabel>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could probably avoid the htmlFor (which I don't think works anyways, since this assumes an ID on the <select>) by including the <FormSelect /> render as a child of the <FormLabel />.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems easier to add the id. Nesting it would require style updates since the bottom margin of the label element doesn't work with the select anymore.

Copy link
Contributor

@aduth aduth Oct 5, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems easier to add the id.

Not super easy in React, since multiple instances of the component could be present on the page, and the ID needs to be unique. So at a minimum it becomes something like:

class MyComponent extends Component {
    static instances = 0;

    componentWillMount() {
        this.instance = ++this.constructor.instances;
    }

    render() {
        return <input id={ this.instance } />
    }
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oy

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like it is assumed that there will ever be only one instance of each form field: https://github.com/Automattic/wp-calypso/blob/master/client/my-sites/upgrades/components/form/input.jsx#L81

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, in that case the name would need to be a required prop, otherwise the htmlFor will be ineffective if omitted. Seems like a nuisance from an implementer's perspective. I personally think we should go the nesting route even if it requires style updates.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@aduth aduth changed the title Wire up reduxified country states Components: Wire up reduxified country states Oct 5, 2016
@aduth
Copy link
Contributor

aduth commented Oct 5, 2016

We should plan to move <StateSelect /> from client/components to client/blocks but I think it'd be fine for a subsequent pull request.

@aduth
Copy link
Contributor

aduth commented Oct 5, 2016

I don't have a site with a wp.com-based domain. Any tipps on how I can test that?

https://wordpress.com/start ? 😄

@aduth
Copy link
Contributor

aduth commented Oct 5, 2016

Confirmed that the select works well during the domain registration step 👍

@aduth aduth added [Status] Needs Author Reply and removed [Status] Needs Review The PR is ready for review. This also triggers e2e canary tests and wp-desktop tests automatically. labels Oct 5, 2016
module.exports = connect(
( state, { countryCode } ) => ( {
countryStates: countryCode ? getCountryStates( state, countryCode ) : []
} )
Copy link
Contributor

@gwwar gwwar Oct 5, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't forget that we can use fancy es6/es2015 features because we use babel. In this case let's use export over module.exports

export default connect(
    ( state, { countryCode } ) => {
         return countryStates: countryCode ? getCountryStates( state, countryCode ) : []
    },
    { recordGoogleEvent } // this can be an object 
)( localize( StateSelect ) );

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gwwar
Copy link
Contributor

gwwar commented Oct 5, 2016

This works well for me, but I noticed we don't have enough room for the descender height in this font size/weight combo.
screen shot 2016-10-05 at 12 10 58 pm

@tyxla
Copy link
Member

tyxla commented Oct 5, 2016

The height issue might be the same that this PR attempts to address: #8470.

@gwwar
Copy link
Contributor

gwwar commented Oct 5, 2016

Thanks for the ping @tyxla. Let's resolve the styling issue in 8470 then

@obenland obenland added [Status] Needs Review The PR is ready for review. This also triggers e2e canary tests and wp-desktop tests automatically. and removed [Status] Needs Author Reply labels Oct 5, 2016
@obenland
Copy link
Member Author

obenland commented Oct 5, 2016

Tested successfully when editing existing contact information. Final sign-off?

@gwwar
Copy link
Contributor

gwwar commented Oct 5, 2016

Looks good! :shipit:

@gwwar gwwar added [Status] Ready to Merge and removed [Status] Needs Review The PR is ready for review. This also triggers e2e canary tests and wp-desktop tests automatically. labels Oct 5, 2016
@obenland obenland merged commit 7504691 into master Oct 6, 2016
@obenland obenland deleted the update/7922-country-states-usage-refactor branch October 6, 2016 17:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants